﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Text;

namespace Cookbook.Common
{
    /// <summary>
    /// Provides helper methods for generating a full process dump, including stack details and the exception hierarchy.
    /// </summary>
    public static class ExceptionHelper
    {
        /// <summary>
        /// Generates a full process dump including all threads and modules.
        /// </summary>
        /// <param name="exception">The exception hierarchy to include in the process dump.</param>
        /// <param name="origin">The origin of the exception, e.g. client or server.</param>
        /// <returns></returns>
        public static string[] GenerateProcessDump(Exception exception, string origin)
        {
            // Keep a reference which we can use to traverse the inner 
            // exceptions.
            Exception tempException = exception;

            // Create some storage to put the process dump in. We don't want
            // to call any SystemMessage objects until all threads have been
            // resumed. If we do, we might run into a deadlock. 
            var bucket = new List<string>();

            // Recursively traverse all exceptions in the exception 
            // hierarchy.
            while (tempException != null)
            {
                Add(bucket, "CALL STACK OF {0} EXCEPTION {1}: {2}",
                    origin.ToUpper(), tempException.GetType().Name, tempException.Message);

                if (tempException.StackTrace != null)
                {
                    string[] frames = tempException.StackTrace.Split('\n');
                    foreach (string frame in frames)
                    {
                        Add(bucket, frame.Replace("\r", ""));
                    }
                }
                else
                {
                    Add(bucket, "    (no stacktrace available)");
                }

                Add(bucket, "END OF CALL STACK");

                tempException = tempException.InnerException;
            }
#if SILVERLIGHT
            Add(bucket, "CALL STACK OF UI THREAD");
#else
            Add(bucket, "CALL STACK OF CURRENT THREAD");
#endif
            GenerateStackDump(bucket, 4);
            Add(bucket, "END OF CALL STACK");

            return bucket.ToArray();
        }

        /// <summary>
        /// Helper function that adds a formatted string to an 
        /// <see cref="System.Collections.Generic.IList{T}"/>.
        /// </summary>
        private static void Add(IList list, string message, params object[] arguments)
        {
            list.Add(string.Format(message, arguments));
        }

        /// <summary>
        /// Generates a stack dump of the specified thread.
        /// </summary>
        /// <remarks>
        /// The format of the stack dump will look similar to the StackTrace 
        /// property of an exception.
        /// </remarks>
        /// <param name="framesToSkip">The number of stack frames to skip 
        /// starting with the most recent one.</param>
        /// <param name="bucket">String-based list for storing the results.
        /// </param>
        private static void GenerateStackDump(IList bucket, int framesToSkip)
        {
            // Get the stack trace and iterate through its frames.
#if SILVERLIGHT
            var stack = new StackTrace();
#else
            var stack = new StackTrace(true);
#endif
            for (
                int frameIndex = framesToSkip;
                frameIndex < stack.FrameCount;
                ++frameIndex)
            {
                // Get the frame and corresponding method
                StackFrame frame = stack.GetFrame(frameIndex);
                MethodBase method = frame.GetMethod();

                var b = new StringBuilder("    at ");

                // Begin with the fully qualified name of the method (including
                // namespace and declaring type).
                string fullName = (method.DeclaringType != null) ? method.DeclaringType.FullName : "<unknown>";
                b.AppendFormat("{0}.{1}(", fullName, method.Name);

                // Add the name and type of all the arguments to the method
                // description.
                ParameterInfo[] parInfoList = method.GetParameters();
                for (int i = 0; i < parInfoList.Length; ++i)
                {
                    ParameterInfo info = parInfoList[i];
                    if (i > 0)
                    {
                        b.AppendFormat(", ");
                    }

                    b.AppendFormat(
                        "{0} {1}",
                        info.ParameterType.Name,
                        info.Name);
                }
                b.Append(") in ");

#if !SILVERLIGHT
                // Try to find the filename and linenumber and add this to the
                // description as well. Notice that this only works if the PDB
                // is there.
                string fileName = frame.GetFileName();
                if (fileName != null)
                {
                    b.Append(fileName);
                    b.Append(":");
                    b.Append(frame.GetFileLineNumber());
                }
#endif

                bucket.Add(b.ToString());
            }
        }
    }
}